package tcln.gxtx.client.widget.form;

import java.util.ArrayList;
import java.util.List;

import tcln.gxtx.client.utils.async.DeferredCommandScheduler;

import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.data.BaseModelData;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.FieldEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.widget.Layout;
import com.extjs.gxt.ui.client.widget.LayoutContainer;
import com.extjs.gxt.ui.client.widget.form.ComboBox;
import com.extjs.gxt.ui.client.widget.form.Field;
import com.extjs.gxt.ui.client.widget.form.MultiField;
import com.extjs.gxt.ui.client.widget.layout.LayoutData;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Focusable;

/**
 * A multifield with the possibility to have a custom layout, allows for more flexible placing of child field elements.
 * 
 * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
 */
public class CustomLayoutMultiField extends MultiField<CustomLayoutMultiField.MultiFieldModelData> {

    protected Layout layout;
    protected List<LayoutData> layoutData;
    protected Field<?> focusedField;

    /**
     * Creates a new multi-field.
     */
    public CustomLayoutMultiField() {
        super();
        layoutData = new ArrayList<LayoutData>();
    }

    /**
     * Creates a new multi-field.
     * 
     * @param fieldLabel
     *            the field label
     * @param fields
     *            the field(s) to add
     */
    public CustomLayoutMultiField(String fieldLabel, Field<?>... fields) {
        super(fieldLabel, fields);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public void setValue(MultiFieldModelData value) {
        for (int i = 0; i < fields.size(); i++) {
            Field field = fields.get(i);
            field.setValue(value.getFieldValue(i));
        }
        super.setValue(value);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public MultiFieldModelData getValue() {
        MultiFieldModelData returnValue = new MultiFieldModelData();
        for (int i = 0; i < fields.size(); i++) {
            Field<?> field = fields.get(i);
            returnValue.setFieldValue(i, field.getValue());
        }
        returnValue.setData(value.getData());
        return returnValue;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setRawValue(String value) {
        // do nothing for this field group
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getRawValue() {
        return "";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void add(Field<?> field) {
        super.add(field);
        layoutData.add(null);
    }

    /**
     * Adds a field with layout data to be used with a custom layout.
     * 
     * @param field
     *            the field to add
     * @param data
     *            the layout data
     */
    public void add(Field<?> field, LayoutData data) {
        super.add(field);
        layoutData.add(data);
    }

    /**
     * Sets layout for this field.
     * 
     * @param layout
     */
    public void setLayout(Layout layout) {
        this.layout = layout;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void focus() {
        if (focusedField == null) { // don't move focus if the user focuses on this field again
            for (Field<?> f : fields) {
                if (f instanceof Focusable) {
                    focusedField = f;
                    break;
                }
            }
            if (focusedField == null) {
                focusedField = fields.get(0);
            }

            setFocusOnField(focusedField);
        }
    }

    /**
     * Checks if the focus is putting on the field at the given index.
     * 
     * @param index
     * @return
     */
    public boolean hasFocusOnField(int index) {
        Field<?> field = fields.get(index);
        if (field instanceof FocusableField) {
            return ((FocusableField) field).hasFocus();
        }
        return (focusedField != null) && (focusedField == field);
    }

    /**
     * Set focus on a field whose index is given.
     * 
     * @param fieldIndex
     */
    public void focusField(final int fieldIndex) {
        if (!hasFocusOnField(fieldIndex)) {
            DeferredCommandScheduler.scheduleOnce(new Command() {
                @Override
                public void execute() {
                    Field<?> field = fields.get(fieldIndex);
                    setFocusOnField(field);
                }
            });
        }
    }

    /**
     * Check the field in the given index if it can receive focus.
     * 
     * @param index
     * @return
     */
    public boolean isFocusable(int index) {
        if (index < fields.size()) {
            return (fields.get(index) instanceof FocusableField);
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void blur() {
        focusable = false;
        focusedField = null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onRender(Element target, int index) {

        // if no custom layout is set do the default
        if (layout == null) {
            super.onRender(target, index);
        } else {

            lc = new LayoutContainer(layout);
            lc.setBorders(false);

            if (GXT.isIE) {
                lc.setStyleAttribute("position", "relative");
            }

            for (int i = 0, len = fields.size(); i < len; i++) {
                final Field<?> f = fields.get(i);

                final LayoutData data = layoutData.get(i);
                lc.add(f, data);
            }

            lc.render(target, index);
            lc.layout();
            setElement(lc.getElement());
        }

        // register specialkey event from child fields. This delegate is useful if this multi-field is used inside a
        // cell-editor because when this field is focused, the first textual child field is focused.
        for (final Field<?> field : fields) {
            field.addListener(Events.SpecialKey, new Listener<BaseEvent>() {
                @Override
                public void handleEvent(BaseEvent be) {
                    if (be.getSource() == field) {
                        CustomLayoutMultiField.this.fireKey((FieldEvent) be);
                    }
                }
            });
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onResize(int width, int height) {
        if (layout == null) {
            super.onResize(width, height);
        } else {

            // if layout container exists (i.e. after rendering), resize it accordingly
            if (lc != null) {
                lc.setSize(width, height);
            }

            if (mask) {
                mask(maskMessage, maskMessageStyleName);
            }
            if (errorIcon != null && errorIcon.isAttached()) {
                alignErrorIcon();
            }
            if (GXT.isIE) {
                el().repaint();
            }
        }
    }

    private void setFocusOnField(Field<?> field) {
        focusable = true;
        focusedField = field;
        field.focus();

        // if focusing field is a combobox, the requirement is that it should be expanded automatically
        if (field instanceof ComboBox<?>) {
            ComboBox<ModelData> comboBoxField = (ComboBox) field;
            ModelData selectedValue = comboBoxField.getValue();

            comboBoxField.expand();
            if (selectedValue != null) {
                comboBoxField.select(selectedValue);
            } else {
                comboBoxField.select(0);
            }
        }
    }

    /**
     * A simple model data for this multi-field.
     * 
     * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
     */
    public static class MultiFieldModelData extends BaseModelData {
        private static final String PROPERTY_DATA = "data";
        private static final String PROPERTY_FIELD = "field-";
        private static final long serialVersionUID = 1L;

        /**
         * Sets value for a field identified by field index.
         * 
         * @param fieldIndex
         *            Index of the field in current field list of a {@link MultiField}.
         * @param value
         *            New value to set.
         */
        public void setFieldValue(int fieldIndex, Object value) {
            set(PROPERTY_FIELD + fieldIndex, value);
        }

        /**
         * Gets a field value.
         * 
         * @param fieldIndex
         *            Index of the field in current field list of a {@link MultiField}.
         * @return Field value.
         */
        public Object getFieldValue(int fieldIndex) {
            return (Object) get(PROPERTY_FIELD + fieldIndex);
        }

        /**
         * Returns current data value of this field.
         * 
         * @return the data.
         */
        public Object getData() {
            return (Object) get(PROPERTY_DATA);
        }

        /**
         * Sets new data value.
         * 
         * @param data
         *            the data to set.
         */
        public void setData(Object data) {
            set(PROPERTY_DATA, data);
        }

    }
}
